<?php

/**
 * Maikuro Framework
 *
 * LICENSE:
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * The license is bundled with this package in the file LICENSE.txt.
 * You may also obtain a copy of the License at this URL:
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * @copyright  Copyright (c) 2010, Jonathan Hedrén
 * @author     Jonathan Hedrén <jonathan.hedren@gmail.com>
 * @license    http://www.apache.org/licenses/LICENSE-2.0 Apache License, Version 2.0
 */

/**
 * @see Request
 */
require_once 'Request.php';

/**
 * @see Response
 */
require_once 'Response.php';

/**
 * @see View
 */
require_once 'View.php';

/**
 * The FrontController class is a core component in the Maikuro framework. It
 * handles the request flow and is responsible for sending the response.
 */
class FrontController
{
    /**
     * The default view name.
     *
     * @var string
     */
    protected $defaultViewName = 'index';

    /**
     * Is used to determine if the FrontController already is dispatching the
     * Request when calling the dispatch method.
     *
     * @var boolean
     */
    private $dispatching = false;

    /**
     * The name of the error view.
     *
     * @var string
     */
    protected $errorViewName = 'error';

    /**
     * The front controller instance
     *
     * @var FrontController
     */
    static protected $instance;

    /**
     * Any (optional) modules.
     *
     * @var array
     */
    private $modules;

    /**
     * The request
     *
     * @var Request
     */
    private $request;

    /**
     * The response
     *
     * @var Response
     */
    private $response;

    /**
     * An optional array of rewrite rules where the key is the path to match and
     * the value is the path to rewrite to.
     *
     * @var array
     */
    private $rewrites;

    /**
     * Whether render thrown exceptions or not. If set to false the request will
     * be forwarded to the error view in case of a thrown exception.
     *
     * @var boolean
     */
    protected $throwExceptions = false;

    /**
     * The view.
     *
     * @var View
     */
    private $view;

    /**
     * Private constructor to enforce singleton.
     */
    private function __construct()
    {
    }

    /**
     * Dispatches the request.
     *
     * @return void
     */
    public function dispatch()
    {
        if ($this->dispatching) {
            throw new RuntimeException('The FrontController is already dispatching the request.');
        }
        $this->dispatching = true;

        $request = $this->getRequest();
        $response = $this->getResponse();

        $dispatchLoops = 0;
        do {
            $view = $this->route($request);
            $request->setDispatched(true);

            try {
                $view->parse();
            } catch (Exception $e) {
                if ($this->throwExceptions) {
                    $request->setDispatched(true);
                    $response->setException($e);
                } else {
                    $request->setPathInfo($this->errorViewName . '/')
                            ->setDispatched(false);
                    continue;
                }
            }
        } while (!$request->isDispatched() && $dispatchLoops++ < 100);

        if (!$response->hasBody()) {
            $response->setBody($view->getOutput());
        }

        $response->send();
    }

    /**
     * Returns the front controller singleton instance.
     *
     * @return FrontController
     */
    static public function getInstance()
    {
        if (self::$instance === null) {
            self::$instance = new self();
        }
        return self::$instance;
    }

    /**
     * Returns any modules.
     *
     * @return array
     */
    public function getModules()
    {
        return $this->modules;
    }

    /**
     * Returns the request. Creates a new Request instance if no request is set.
     *
     * @return Request
     */
    public function getRequest()
    {
        if ($this->request === null) {
            $this->request = new Request();
        }
        return $this->request;
    }

    /**
     * Returns the response. Create a new Response instance if no response is
     * set.
     *
     * @return Response
     */
    public function getResponse()
    {
        if ($this->response === null) {
            $this->response = new Response();
        }
        return $this->response;
    }

    /**
     * Returns any rewrites.
     *
     * @return array
     */
    public function getRewrites()
    {
        return $this->rewrites;
    }

    /**
     * Returns the view. Creates a new View instance if no view is set.
     *
     * @return View
     */
    public function getView()
    {
        if ($this->view === null) {
            $this->view = new View($this->getRequest(), $this->getResponse());
        }
        return $this->view;
    }

    /**
     * Routes the request:
     * 1. Creates key/value-pair parameters from the path info.
     * 2. Finds out which view to parse.
     *
     * @return string
     */
    public function route(Request $request)
    {
        $path = $request->getPathInfo();

        $rewrites = $this->getRewrites();

        if ($rewrites !== null) {
            foreach ($rewrites as $from => $to) {
                if ($path == trim($from, '/')) {
                    $path = $to;
                    break;
                }
            }
        }

        $view = $this->getView();

        $pathParts = explode('/', $path);

        if (count($pathParts) && $pathParts[0] !== '') {

            $modules = $this->getModules();

            $slice = 1;
            if ($modules !== null
                && in_array($pathParts[0], $modules)) {
                    $slice = 2;
            }

            $params = array_slice($pathParts, $slice, count($pathParts));
            /**
             * If $params is odd, there will be a value missing,
             * so we asume the value is true.
             */
            if (count($params) % 2) {
                $params[] = true;
            }

            $mappedParams = array();
            $i = 0;
            while ($i < count($params)) {
                $mappedParams[$params[$i++]] = $params[$i++];
            }

            $request->setParams(array_merge($request->getParams(), $mappedParams));

            $viewParts = array_slice($pathParts, 0, $slice);

            if (count($viewParts) > 1) {
                $view->setModule($viewParts[0])
                     ->setName($viewParts[1]);
            } else if ($modules !== null && in_array($viewParts[0], $modules)) {
                    $view->setModule($viewParts[0])
                         ->setName($this->defaultViewName);
            } else {
                $view->setName($viewParts[0]);
            }

        } else {
            $view->setName($this->defaultViewName);
        }
        
        return $view;
    }

    /**
     * Sets the default view name.
     *
     * @param string $defaultViewName
     * @return FrontController
     */
    public function setDefaultViewName($defaultViewName)
    {
        if ($defaultViewName === null || $defaultViewName === '') {
            throw new Exception('The default view can not be empty.');
        }
        $this->defaultViewName = $defaultViewName;
        return $this;
    }

    /**
     * Sets the error view name.
     *
     * @param string $errorViewName
     * @return FrontController
     */
    public function setErrorViewName($errorViewName)
    {
        if ($errorViewName === null || $errorViewName === '') {
            throw new Exception('The error view can not be empty.');
        }
        $this->errorViewName = $errorViewName;
        return $this;
    }

    /**
     * Sets any modules.
     *
     * @param array $modules
     * @return FrontController
     */
    public function setModules(array $modules)
    {
        $this->modules = $modules;
        return $this;
    }

    /**
     * Sets the request.
     *
     * @param Request $request
     * @return FrontController
     */
    public function setRequest(Request $request)
    {
        $this->request = $request;
        return $this;
    }

    /**
     * Sets the response.
     *
     * @param Response $response
     * @return FrontController
     */
    public function setResponse(Response $response)
    {
        $this->response = $response;
        return $this;
    }

    /**
     * Sets an array of rewrites where the key is the pretty url and the value
     * is the ugly url.
     *
     * @param array $rewrites
     * @return FrontController
     */
    public function setRewrites(array $rewrites)
    {
        $this->rewrites = $rewrites;
        return $this;
    }

    /**
     * Sets the view.
     *
     * @param View $view
     * @return FrontController
     */
    public function setView(View $view)
    {
        $this->view = $view;
        return $this;
    }

    /**
     * Sets whether to render threwn exceptions or not.
     *
     * @param boolean $throwExceptions
     * @return FrontController
     */
    public function throwExceptions($throwExceptions = true)
    {
        $this->throwExceptions = $throwExceptions;
        return $this;
    }
}
